﻿using Microsoft.Extensions.Caching.Distributed;
using Microsoft.Extensions.Caching.StackExchangeRedis;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Options;
using StackExchange.Redis;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Volo.Abp;
using Volo.Abp.Caching.StackExchangeRedis;

namespace Xms.Abp.Caching;

public class XmsRedisCache : AbpRedisCache, IXmsCache
{
    public string DataKeyName => DataKey;
    public XmsRedisCache(IOptions<RedisCacheOptions> optionsAccessor, IConfiguration configuration)
        : base(CreateOptions(optionsAccessor, configuration))
    {
    }

    private static IOptions<RedisCacheOptions> CreateOptions(IOptions<RedisCacheOptions> optionsAccessor, IConfiguration configuration)
    {
        if (optionsAccessor.Value.Configuration.IsNullOrWhiteSpace())
        {
            optionsAccessor.Value.Configuration = configuration.GetConnectionString("DefaultCache");
        }
        return Options.Create(optionsAccessor.Value);
    }

    public async Task<bool> KeyExistsAsync(string key, CancellationToken cancellationToken = default)
    {
        await ConnectAsync(cancellationToken);

        key = key.StartsWith(Instance) ? key : (Instance + key);

        return await RedisDatabase.KeyExistsAsync(key);
    }

    public async Task<DistributedCacheEntryOptions> GetExpirationFromCache(string key, CancellationToken cancellationToken = default)
    {
        await ConnectAsync(cancellationToken);

        key = key.StartsWith(Instance) ? key : (Instance + key);
        RedisValue[] results = (await RedisDatabase.HashMemberGetManyAsync(new[] { key }, AbsoluteExpirationKey, SlidingExpirationKey))[0];
        if (results.Length >= 2)
        {
            MapMetadata(results, out DateTimeOffset? absExpr, out TimeSpan? sldExpr);

            return new DistributedCacheEntryOptions { AbsoluteExpiration = absExpr, SlidingExpiration = sldExpr };
        }
        return null;
    }

    public async Task<byte[]> GetHashAsync(string key, string field, CancellationToken cancellationToken = default)
    {
        return (await GetHashManyAsync(key, new[] { field })).FirstOrDefault().Value;
    }

    public async Task<IEnumerable<KeyValuePair<string, byte[]>>> GetHashManyAsync(string key, IEnumerable<string> fields, CancellationToken cancellationToken = default)
    {
        key = Check.NotNull(key, nameof(key));
        fields = Check.NotNull(fields, nameof(fields));
        return await GetAndRefreshHashManyAsync(key, fields, cancellationToken);
    }

    public async Task<IEnumerable<KeyValuePair<string, byte[]>>> GetHashAllAsync(string key, CancellationToken cancellationToken = default)
    {
        key = Check.NotNull(key, nameof(key));
        return await GetAndRefreshHashAllAsync(key, cancellationToken);
    }

    public async Task<IEnumerable<string>> GetHashKeysAsync(string key, CancellationToken cancellationToken = default)
    {
        key = Check.NotNull(key, nameof(key));
        var keys = await RedisDatabase.HashKeysAsync(key);
        return keys.Select(r => (string)r);
    }

    public async Task SetHashAsync(string key, string field, byte[] value, CancellationToken cancellationToken = default)
    {
        await SetHashManyAsync(key, new[] { new KeyValuePair<string, byte[]>(field, value) }, cancellationToken);
    }

    public async Task SetHashManyAsync(string key, IEnumerable<KeyValuePair<string, byte[]>> items, CancellationToken cancellationToken = default)
    {
        cancellationToken.ThrowIfCancellationRequested();

        await ConnectAsync(cancellationToken);

        await PipelineSetHashMany(key, items);
    }

    protected virtual async Task<IEnumerable<KeyValuePair<string, byte[]>>> GetAndRefreshHashManyAsync(
        string key,
        IEnumerable<string> fields,
        CancellationToken cancellationToken = default)
    {
        cancellationToken.ThrowIfCancellationRequested();
        await ConnectAsync(cancellationToken);

        key = key.StartsWith(Instance) ? key : (Instance + key);
        var keyArray = new string[] { key };

        var fieldList = fields.ToList();
        fieldList.Insert(0, SlidingExpirationKey);
        fieldList.Insert(0, AbsoluteExpirationKey);

        var fieldArray = fieldList.ToArray();

        RedisValue[] results = (await RedisDatabase.HashMemberGetManyAsync(keyArray, fieldArray))[0];

        await PipelineRefreshManyAndOutHash(key, fields.ToArray(), results, out var result);

        return result;
    }

    protected virtual Task PipelineRefreshManyAndOutHash(
        string key,
        string[] fields,
        RedisValue[] values,
        out Dictionary<string, byte[]> result)
    {
        result = new Dictionary<string, byte[]>();
        Task task = Task.CompletedTask;

        if (values.Length >= 2)
        {
            MapMetadata(values.Take(2).ToArray(), out DateTimeOffset? absExpr, out TimeSpan? sldExpr);

            if (sldExpr.HasValue)
            {
                TimeSpan? expr;

                if (absExpr.HasValue)
                {
                    var relExpr = absExpr.Value - DateTimeOffset.Now;
                    expr = relExpr <= sldExpr.Value ? relExpr : sldExpr;
                }
                else
                {
                    expr = sldExpr;
                }

                task = RedisDatabase.KeyExpireAsync(key, expr);
            }
        }

        for (var i = 0; i < fields.Length; i++)
        {
            if (values.Length >= 3 && values[i + 2].HasValue)
            {
                result[fields[i]] = values[i + 2];
            }
            else
            {
                result[fields[i]] = null;
            }
        }

        return task;
    }

    protected async Task PipelineSetHashMany(string key, IEnumerable<KeyValuePair<string, byte[]>> items)
    {
        key = key.StartsWith(Instance) ? key : (Instance + key);
        await RedisDatabase.HashSetAsync(key, items.Select(r => new HashEntry(r.Key, r.Value)).ToArray());
        var expires = await GetExpirationFromCache(key);
        RedisValue[] results = (await RedisDatabase.HashMemberGetManyAsync(new[] { key }, AbsoluteExpirationKey, SlidingExpirationKey))[0];
        if (expires.SlidingExpiration.HasValue)
        {
            TimeSpan? expr;

            if (expires.AbsoluteExpiration.HasValue)
            {
                var relExpr = expires.AbsoluteExpiration.Value - DateTimeOffset.Now;
                expr = relExpr <= expires.SlidingExpiration.Value ? relExpr : expires.SlidingExpiration;
            }
            else
            {
                expr = expires.SlidingExpiration;
            }

            await RedisDatabase.KeyExpireAsync(key, expr);
        }
    }

    protected virtual async Task<IEnumerable<KeyValuePair<string, byte[]>>> GetAndRefreshHashAllAsync(
        string key,
        CancellationToken cancellationToken = default)
    {
        cancellationToken.ThrowIfCancellationRequested();
        await ConnectAsync(cancellationToken);

        key = key.StartsWith(Instance) ? key : (Instance + key);

        HashEntry[] results = (await RedisDatabase.HashGetAllAsync(key));

        await PipelineRefreshManyAndOutHash(key, results, out var result);

        return result;
    }

    protected virtual Task PipelineRefreshManyAndOutHash(
        string key,
        IEnumerable<HashEntry> entries,
        out Dictionary<string, byte[]> result)
    {
        Task task = Task.CompletedTask;

        if (entries.Any(r => r.Name == SlidingExpirationKey) && entries.Any(r => r.Name == AbsoluteExpirationKey))
        {
            var slidingExpire = entries.First(r => r.Name == SlidingExpirationKey).Value;
            var absExpire = entries.First(r => r.Name == AbsoluteExpirationKey).Value;
            MapMetadata(new[] { slidingExpire, absExpire }, out DateTimeOffset? absExpr, out TimeSpan? sldExpr);

            if (sldExpr.HasValue)
            {
                TimeSpan? expr;

                if (absExpr.HasValue)
                {
                    var relExpr = absExpr.Value - DateTimeOffset.Now;
                    expr = relExpr <= sldExpr.Value ? relExpr : sldExpr;
                }
                else
                {
                    expr = sldExpr;
                }

                task = RedisDatabase.KeyExpireAsync(key, expr);
            }
        }

        result = entries
            .Where(r => r.Name != SlidingExpirationKey && r.Name != AbsoluteExpirationKey && !(r.Name == "data" && r.Value == "\"HASH\""))
            .ToDictionary(r => (string)r.Name, r => (byte[])r.Value);

        return task;
    }

    public async Task<bool> HashFieldExsitsAsync(string key, string field, CancellationToken cancellationToken = default)
    {
        return await RedisDatabase.HashExistsAsync(key, field);
    }

    public async Task RemoveHashAsync(string key, string field, CancellationToken cancellationToken = default)
    {
        await RedisDatabase.HashDeleteAsync(key, field);
    }

    public async Task SetGeoAsync(string key, string member, double longitude, double latitude, CancellationToken cancellationToken = default)
    {
        await ConnectAsync(cancellationToken);
        await RedisDatabase.GeoAddAsync(key, longitude, latitude, member);
    }
    public async Task SetGeoAsync(string key, string member, GeoPositionItem position, CancellationToken cancellationToken = default)
    {
        await ConnectAsync(cancellationToken);
        await RedisDatabase.GeoAddAsync(key, position.Longitude, position.Latitude, member);
    }
    public async Task SetGeoManyAsync(string key, IEnumerable<KeyValuePair<string, GeoPositionItem>> geos, CancellationToken cancellationToken = default)
    {
        await ConnectAsync(cancellationToken);
        await RedisDatabase.GeoAddAsync(key, geos.Select(r => new GeoEntry(r.Value.Longitude, r.Value.Latitude, r.Key)).ToArray());
    }
    public async Task<GeoPositionItem> GetGeoAsync(string key, string member, CancellationToken cancellationToken = default)
    {
        await ConnectAsync(cancellationToken);
        var position = await RedisDatabase.GeoPositionAsync(key, member);
        return Parse(position);
    }
    private static GeoPositionItem Parse(GeoPosition? position)
    {
        return position.HasValue ? new GeoPositionItem
        {
            Latitude = position.Value.Latitude,
            Longitude = position.Value.Longitude,
        } : null;
    }
    private static GeoPositionItem[] Parse(GeoPosition?[] positions)
    {
        return positions.Select(r => Parse(r)).ToArray();
    }
    public async Task<IEnumerable<KeyValuePair<string, GeoPositionItem>>> GetGeoManyAsync(string key, string[] members, CancellationToken cancellationToken = default)
    {
        await ConnectAsync(cancellationToken);
        var positions = await RedisDatabase.GeoPositionAsync(key, members.OfType<RedisValue>().ToArray());
        var ps = Parse(positions);
        var result = new Dictionary<string, GeoPositionItem>();
        for (int i = 0; i < ps.Length; i++)
        {
            result[members[i]] = ps[i];
        }
        return result;
    }
    public async Task<IEnumerable<KeyValuePair<string, GeoPositionItem>>> GetGeoManyAsync(string key, IEnumerable<string> members, CancellationToken cancellationToken = default)
    {
        await ConnectAsync(cancellationToken);
        var positions = await RedisDatabase.GeoPositionAsync(key, members.OfType<RedisValue>().ToArray());
        var ps = Parse(positions);
        var result = new Dictionary<string, GeoPositionItem>();
        for (int i = 0; i < ps.Length; i++)
        {
            result[members.ElementAt(i)] = ps[i];
        }
        return result;
    }
    public async Task<bool> RemoveGeoAsync(string key, string member, CancellationToken cancellationToken = default)
    {
        await ConnectAsync(cancellationToken);
        return await RedisDatabase.GeoRemoveAsync(key, member);
    }
    public async Task<double?> GeoDistanceAsync(string key, string member1, string member2, CancellationToken cancellationToken = default)
    {
        await ConnectAsync(cancellationToken);
        return await RedisDatabase.GeoDistanceAsync(key, member1, member2, GeoUnit.Meters);
    }
    public async Task<GeoRadiusItem[]> GeoRadiusAsync(string key, string member, double radius, CancellationToken cancellationToken = default)
    {
        await ConnectAsync(cancellationToken);
        var results = await RedisDatabase.GeoRadiusAsync(key, member, radius, GeoUnit.Meters, order: Order.Ascending);
        return Parse(results);
    }
    public async Task<GeoRadiusItem[]> GeoRadiusAsync(string key, double longitude, double latitude, double radius, CancellationToken cancellationToken = default)
    {
        await ConnectAsync(cancellationToken);
        var results = await RedisDatabase.GeoRadiusAsync(key, longitude, latitude, radius, GeoUnit.Meters, order: Order.Ascending);
        return Parse(results);
    }

    private static GeoRadiusItem[] Parse(GeoRadiusResult[] results)
    {
        return results.Select(r => new GeoRadiusItem
        {
            Distance = r.Distance,
            Member = r.Member,
            Position = r.Position.HasValue ? new GeoPositionItem
            {
                Latitude = r.Position.Value.Latitude,
                Longitude = r.Position.Value.Longitude
            } : null,
        }).ToArray();
    }

    private const string LockTaskScriptCode = @"
local existsKey = redis.call('EXISTS', KEYS[1]);
local argvCount = tonumber(ARGV[1]);
if existsKey == 1 then
	local initialCount = tonumber(redis.call('ZRANGE', KEYS[1], 0, 0)[1]);
	if initialCount == argvCount then
		local score = tonumber(redis.call('ZSCORE', KEYS[1], initialCount));
		if score < initialCount then
			redis.call('ZINCRBY', KEYS[1], 1, initialCount);
			return true;
		end
	end
else
	redis.call('ZADD', KEYS[1], 1, argvCount);
	redis.call('PEXPIRE', KEYS[1], ARGV[2]);
	return true;
end
return false;
";
    private const string LockReleaseScriptCode = @"
local existsKey = redis.call('EXISTS', KEYS[1]);
if existsKey == 1 then
	local releaseCount = tonumber(ARGV[1]);
	local initialCount = tonumber(redis.call('ZRANGE', KEYS[1], 0, 0)[1]);
	local score = tonumber(redis.call('ZSCORE', KEYS[1], initialCount));
	local total = score - releaseCount;
	if total > 0 then
		redis.call('ZINCRBY', KEYS[1], -releaseCount, initialCount);
		return true;
	else
		redis.call('DEL', KEYS[1]);
		return total == 0;
	end
end
return false;
";
    private LoadedLuaScript _lockTaskLoadedScript = null;
    private LoadedLuaScript LockTaskLoadedScript
    {
        get
        {
            if (_lockTaskLoadedScript == null)
            {
                var conn = RedisDatabase.Multiplexer;
                var configuration = ConfigurationOptions.Parse(conn.Configuration);
                foreach (var endpoint in configuration.EndPoints)
                {
                    var server = conn.GetServer(endpoint);
                    var lockTaskScript = LuaScript.Prepare(LockTaskScriptCode);
                    _lockTaskLoadedScript = lockTaskScript.Load(server);
                }
            }
            return _lockTaskLoadedScript;
        }
    }
    private LoadedLuaScript _lockReleaseLoadedScript = null;
    private LoadedLuaScript LockReleaseLoadedScript
    {
        get
        {
            if (_lockReleaseLoadedScript == null)
            {
                var conn = RedisDatabase.Multiplexer;
                var configuration = ConfigurationOptions.Parse(conn.Configuration);
                foreach (var endpoint in configuration.EndPoints)
                {
                    var server = conn.GetServer(endpoint);
                    var lockReleaseScript = LuaScript.Prepare(LockReleaseScriptCode);
                    _lockReleaseLoadedScript = lockReleaseScript.Load(server);
                }
            }
            return _lockReleaseLoadedScript;
        }
    }
    public async Task<bool> LockTaskAsync(string key, int initialCount = 1, TimeSpan? expiration = null, CancellationToken cancellationToken = default)
    {
        return (bool)(await RedisDatabase.ScriptEvaluateAsync(LockTaskLoadedScript.Hash, new RedisKey[] { key }, new RedisValue[] { initialCount, (int)((expiration ?? TimeSpan.FromSeconds(30)).TotalMilliseconds) }));
    }

    public async Task<bool> LockReleaseAsync(string key, int releaseCount = 1, CancellationToken cancellationToken = default)
    {
        return (bool)(await RedisDatabase.ScriptEvaluateAsync(LockReleaseLoadedScript.Hash, new RedisKey[] { key }, new RedisValue[] { releaseCount }));
    }

    public async Task<bool> LockExtendAsync(string key, TimeSpan? expiration = null, CancellationToken cancellationToken = default)
    {
        return await RedisDatabase.KeyExpireAsync(key, expiration ?? TimeSpan.FromSeconds(30));
    }

    private const string GetSerialNumberScriptCode = @"
local existsKey = redis.call('EXISTS', KEYS[1]);
local quantity = tonumber(ARGV[1]);
if existsKey == 1 then
    local value = tonumber(redis.call('ZSCORE', KEYS[1], 'VALUE')) + 1;
	redis.call('ZINCRBY', KEYS[1], quantity, 'VALUE');
    return value;
else
	redis.call('ZADD', KEYS[1], quantity, 'VALUE');
    local expiration = tonumber(ARGV[2]);
    if expiration > 0 then
	    redis.call('PEXPIRE', KEYS[1], expiration);
    end
	return 1;
end
";
    private LoadedLuaScript _getSerialNumberLoadedScript = null;
    private LoadedLuaScript GetSerialNumberLoadedScript
    {
        get
        {
            if (_getSerialNumberLoadedScript == null)
            {
                var conn = RedisDatabase.Multiplexer;
                var configuration = ConfigurationOptions.Parse(conn.Configuration);
                foreach (var endpoint in configuration.EndPoints)
                {
                    var server = conn.GetServer(endpoint);
                    var getSerialNumberScript = LuaScript.Prepare(GetSerialNumberScriptCode);
                    _getSerialNumberLoadedScript = getSerialNumberScript.Load(server);
                }
            }
            return _getSerialNumberLoadedScript;
        }
    }
    public async Task<long> GetSerialNumberAsync(string key, int quantity = 1, TimeSpan? expiration = null, CancellationToken cancellationToken = default)
    {
        return (long)(await RedisDatabase.ScriptEvaluateAsync(GetSerialNumberLoadedScript.Hash, new RedisKey[] { key }, new RedisValue[] { quantity, (int)((expiration?.TotalMilliseconds ?? -1)) }));
    }
}

//public class XmsRedisCache<TCacheContext> : XmsRedisCache, IXmsCache<TCacheContext>
//    where TCacheContext : CacheContext<TCacheContext>
//{
//    public XmsRedisCache(IOptions<RedisCacheOptions> optionsAccessor, IConfiguration configuration)
//        : base(optionsAccessor, configuration)
//    {
//    }

//    protected static IOptions<RedisCacheOptions> CreateOptions(IServiceProvider serviceProvider)
//    {
//        var redisOptions = serviceProvider.GetRequiredService<IOptions<RedisCacheOptions>>().Value;
//        var connectionString = AsyncHelper.RunSync(async () => await serviceProvider.GetRequiredService<ICacheConnectionStringProvider<TCacheContext>>().GetConnectionStringAsync());
//        redisOptions.Configuration = connectionString;
//        return Options.Create(redisOptions);
//    }
//}
